#include <string>

#include "common/node_utils.h"
#include "common/pg_guard.h"

extern "C" {
#include <fmgr.h>

#include <access/htup_details.h>
#include <catalog/pg_aggregate.h>
#include <catalog/pg_namespace.h>
#include <catalog/pg_operator.h>
#include <catalog/pg_proc.h>
#include <catalog/pg_type_d.h>
#include <lib/stringinfo.h>
#include <nodes/nodeFuncs.h>
#include <nodes/nodes.h>
#include <optimizer/optimizer.h>
#include <utils/builtins.h>
#include <utils/lsyscache.h>
#include <utils/syscache.h>
#include <utils/typcache.h>
}

#define Str(x) std::to_string((x))

namespace optimizer {

static void ToStringImpl(StringInfo buf, const void* expr);
static void DeparseStringLiteral(StringInfo buf, const char* val);
static void DeparseOperatorName(StringInfo buf, Form_pg_operator opform);
static char* DeparseTypeName(Oid type_oid, int32 typemod);
static void AppendFunctionName(Oid funcid, StringInfo buf);

void DeparseStringLiteral(StringInfo buf, const char* val) {
  const char* valptr;

  /*
   * Rather than making assumptions about the remote server's value of
   * standard_conforming_strings, always use E'foo' syntax if there are any
   * backslashes.  This will fail on remote servers before 8.1, but those
   * are long out of support.
   */
  if (strchr(val, '\\') != nullptr)
    appendStringInfoChar(buf, ESCAPE_STRING_SYNTAX);
  appendStringInfoChar(buf, '\'');
  for (valptr = val; *valptr != 0; valptr++) {
    char ch = *valptr;

    if (SQL_STR_DOUBLE(ch, true))
      appendStringInfoChar(buf, ch);
    appendStringInfoChar(buf, ch);
  }
  appendStringInfoChar(buf, '\'');
}

void DeparseOperatorName(StringInfo buf, Form_pg_operator opform) {
  char* opname;

  /* opname is not a SQL identifier, so we should not quote it. */
  opname = NameStr(opform->oprname);

  /* Print schema name only if it's not pg_catalog */
  if (opform->oprnamespace != PG_CATALOG_NAMESPACE) {
    const char* opnspname;

    opnspname = get_namespace_name(opform->oprnamespace);
    /* Print fully qualified operator name. */
    appendStringInfo(buf, "OPERATOR(%s.%s)", quote_identifier(opnspname), opname);
  } else {
    /* Just print operator name. */
    appendStringInfoString(buf, opname);
  }
}

char* DeparseTypeName(Oid type_oid, int32 typemod) {
  bits16 flags = FORMAT_TYPE_TYPEMOD_GIVEN;

  if (!(type_oid < FirstGenbkiObjectId))
    flags |= FORMAT_TYPE_FORCE_QUALIFY;

  return format_type_extended(type_oid, typemod, flags);
}

void AppendFunctionName(Oid funcid, StringInfo buf) {
  HeapTuple proctup;
  Form_pg_proc procform;
  const char* proname;

  proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
  if (!HeapTupleIsValid(proctup))
    elog(ERROR, "cache lookup failed for function %u", funcid);
  procform = (Form_pg_proc)GETSTRUCT(proctup);

  /* Print schema name only if it's not pg_catalog */
  if (procform->pronamespace != PG_CATALOG_NAMESPACE) {
    const char* schemaname;

    schemaname = get_namespace_name(procform->pronamespace);
    appendStringInfo(buf, "%s.", quote_identifier(schemaname));
  }

  /* Always print the function name */
  proname = NameStr(procform->proname);
  appendStringInfoString(buf, quote_identifier(proname));

  ReleaseSysCache(proctup);
}

static Node* DeparseSortGroupClause(Index ref, List* tlist, bool force_colno, StringInfo buf) {
  TargetEntry* tle;
  Expr* expr;

  tle = get_sortgroupref_tle(ref, tlist);
  expr = tle->expr;

  if (force_colno) {
    /* Use column-number form when requested by caller. */
    Assert(!tle->resjunk);
    appendStringInfo(buf, "%d", tle->resno);
  } else if ((expr != nullptr) && IsA(expr, Const)) {
    ToStringImpl(buf, (Const*)expr);
  } else if ((expr == nullptr) || IsA(expr, Var))
    ToStringImpl(buf, expr);
  else {
    /* Always parenthesize the expression. */
    appendStringInfoChar(buf, '(');
    ToStringImpl(buf, expr);
    appendStringInfoChar(buf, ')');
  }

  return (Node*)expr;
}

static void AppendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first, StringInfo buf) {
  TypeCacheEntry* typentry;

  /* See whether operator is default < or > for sort expr's datatype. */
  typentry = lookup_type_cache(sortcoltype, TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);

  if (sortop == typentry->lt_opr)
    appendStringInfoString(buf, " ASC");
  else if (sortop == typentry->gt_opr)
    appendStringInfoString(buf, " DESC");
  else {
    HeapTuple opertup;
    Form_pg_operator operform;

    appendStringInfoString(buf, " USING ");

    /* Append operator name. */
    opertup = SearchSysCache1(OPEROID, ObjectIdGetDatum(sortop));
    if (!HeapTupleIsValid(opertup))
      elog(ERROR, "cache lookup failed for operator %u", sortop);
    operform = (Form_pg_operator)GETSTRUCT(opertup);
    DeparseOperatorName(buf, operform);
    ReleaseSysCache(opertup);
  }

  if (nulls_first)
    appendStringInfoString(buf, " NULLS FIRST");
  else
    appendStringInfoString(buf, " NULLS LAST");
}

static void AppendAggOrderBy(List* order_list, List* target_list, StringInfo buf) {
  bool first = true;

  foreach_node(SortGroupClause, srt, order_list) {
    Node* sortexpr;

    if (!first)
      appendStringInfoString(buf, ", ");
    first = false;

    /* Deparse the sort expression proper. */
    sortexpr = DeparseSortGroupClause(srt->tleSortGroupRef, target_list, false, buf);
    /* Add decoration as needed. */
    AppendOrderBySuffix(srt->sortop, exprType(sortexpr), srt->nulls_first, buf);
  }
}

void ToStringImpl(StringInfo buf, const void* expr) {
  if (expr == nullptr) {
    appendStringInfoString(buf, "null");
    return;
  }

  if (IsA(expr, List) || IsA(expr, IntList) || IsA(expr, OidList) || IsA(expr, XidList)) {
    const auto* node = (List*)expr;
    appendStringInfoString(buf, "{");
    int idx = 0;
    ListCell* lc;
    foreach (lc, node) {
      if (IsA(node, List))
        ToStringImpl(buf, lfirst(lc));
      else if (IsA(node, IntList))
        appendStringInfo(buf, "%d", lfirst_int(lc));
      else if (IsA(node, OidList))
        appendStringInfo(buf, "%d", lfirst_oid(lc));
      else if (IsA(node, XidList))
        appendStringInfo(buf, "%d", lfirst_xid(lc));

      if (idx++ < list_length(node) - 1)
        appendStringInfoString(buf, ", ");
    }
    appendStringInfoString(buf, "}");
    return;
  }

  switch (nodeTag(expr)) {
    case T_Const: {
      const auto* node = (Const*)expr;
      if (node->constisnull) {
        appendStringInfoString(buf, "null");
        return;
      }

      Oid typoutput;
      bool typ_is_varlena;
      getTypeOutputInfo(node->consttype, &typoutput, &typ_is_varlena);
      auto* extval = OidOutputFunctionCall(typoutput, node->constvalue);
      switch (node->consttype) {
        case INT2OID:
        case INT4OID:
        case INT8OID:
        case OIDOID:
        case FLOAT4OID:
        case FLOAT8OID:
        case NUMERICOID: {
          /*
           * No need to quote unless it's a special value such as 'NaN'.
           * See comments in get_const_expr().
           */
          if (strspn(extval, "0123456789+-eE.") == strlen(extval)) {
            if (extval[0] == '+' || extval[0] == '-')
              appendStringInfo(buf, "(%s)", extval);
            else
              appendStringInfoString(buf, extval);
          } else
            appendStringInfo(buf, "'%s'", extval);
        } break;
        case BITOID:
        case VARBITOID:
          appendStringInfo(buf, "B'%s'", extval);
          break;
        case BOOLOID:
          if (strcmp(extval, "t") == 0)
            appendStringInfoString(buf, "true");
          else
            appendStringInfoString(buf, "false");
          break;
        default:
          DeparseStringLiteral(buf, extval);
          break;
      }
    } break;
    case T_BoolExpr: {
      const auto* node = (BoolExpr*)expr;
      const char* op = nullptr; /* keep compiler quiet */
      bool first;
      ListCell* lc;

      switch (node->boolop) {
        case AND_EXPR:
          op = "AND";
          break;
        case OR_EXPR:
          op = "OR";
          break;
        case NOT_EXPR:
          appendStringInfoString(buf, "(NOT ");
          ToStringImpl(buf, linitial(node->args));
          appendStringInfoChar(buf, ')');
          return;
      }

      appendStringInfoChar(buf, '(');
      first = true;
      foreach (lc, node->args) {
        if (!first)
          appendStringInfo(buf, " %s ", op);
        ToStringImpl(buf, (Expr*)lfirst(lc));
        first = false;
      }
      appendStringInfoChar(buf, ')');
    } break;
    case T_OpExpr: {
      const auto* node = (OpExpr*)expr;
      HeapTuple tuple;
      Form_pg_operator form;
      Expr* right;
      char oprkind;

      /* Retrieve information about the operator from system catalog. */
      tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
      if (!HeapTupleIsValid(tuple))
        elog(ERROR, "cache lookup failed for operator %u", node->opno);
      form = (Form_pg_operator)GETSTRUCT(tuple);
      oprkind = form->oprkind;

      /* Sanity check. */
      Assert((oprkind == 'l' && list_length(node->args) == 1) || (oprkind == 'b' && list_length(node->args) == 2));

      right = (Expr*)llast(node->args);

      /* Always parenthesize the expression. */
      appendStringInfoChar(buf, '(');

      /* Deparse left operand, if any. */
      if (oprkind == 'b') {
        Expr* left = (Expr*)linitial(node->args);

        ToStringImpl(buf, left);

        appendStringInfoChar(buf, ' ');
      }

      /* Deparse operator name. */
      DeparseOperatorName(buf, form);

      /* Deparse right operand. */
      appendStringInfoChar(buf, ' ');

      ToStringImpl(buf, right);

      appendStringInfoChar(buf, ')');

      ReleaseSysCache(tuple);
    } break;
    case T_NullTest: {
      const auto* node = (NullTest*)expr;
      appendStringInfoChar(buf, '(');
      ToStringImpl(buf, node->arg);

      /*
       * For scalar inputs, we prefer to print as IS [NOT] NULL, which is
       * shorter and traditional.  If it's a rowtype input but we're applying a
       * scalar test, must print IS [NOT] DISTINCT FROM NULL to be semantically
       * correct.
       */
      if (node->argisrow || !type_is_rowtype(exprType((Node*)node->arg))) {
        if (node->nulltesttype == IS_NULL)
          appendStringInfoString(buf, " IS NULL)");
        else
          appendStringInfoString(buf, " IS NOT NULL)");
      } else {
        if (node->nulltesttype == IS_NULL)
          appendStringInfoString(buf, " IS NOT DISTINCT FROM NULL)");
        else
          appendStringInfoString(buf, " IS DISTINCT FROM NULL)");
      }
    } break;
    case T_ArrayExpr: {
      const auto* node = (ArrayExpr*)expr;
      bool first = true;
      ListCell* lc;

      appendStringInfoString(buf, "ARRAY[");
      foreach (lc, node->elements) {
        if (!first)
          appendStringInfoString(buf, ", ");
        ToStringImpl(buf, lfirst(lc));
        first = false;
      }
      appendStringInfoChar(buf, ']');

      /* If the array is empty, we need an explicit cast to the array type. */
      if (node->elements == NIL)
        appendStringInfo(buf, "::%s", DeparseTypeName(node->array_typeid, -1));
    } break;
    case T_Aggref: {
      const auto* node = (Aggref*)expr;
      bool use_variadic;

      /* Only basic, non-split aggregation accepted. */
      Assert(node->aggsplit == AGGSPLIT_SIMPLE);

      /* Check if need to print VARIADIC (cf. ruleutils.c) */
      use_variadic = node->aggvariadic;

      /* Find aggregate name from aggfnoid which is a pg_proc entry */
      AppendFunctionName(node->aggfnoid, buf);
      appendStringInfoChar(buf, '(');

      /* Add DISTINCT */
      appendStringInfoString(buf, (node->aggdistinct != NIL) ? "DISTINCT " : "");

      if (AGGKIND_IS_ORDERED_SET(node->aggkind)) {
        /* Add WITHIN GROUP (ORDER BY ..) */
        ListCell* arg;
        bool first = true;

        Assert(!node->aggvariadic);
        Assert(node->aggorder != NIL);

        foreach (arg, node->aggdirectargs) {
          if (!first)
            appendStringInfoString(buf, ", ");
          first = false;

          ToStringImpl(buf, (Expr*)lfirst(arg));
        }

        appendStringInfoString(buf, ") WITHIN GROUP (ORDER BY ");
        AppendAggOrderBy(node->aggorder, node->args, buf);
      } else {
        /* aggstar can be set only in zero-argument aggregates */
        if (node->aggstar)
          appendStringInfoChar(buf, '*');
        else {
          ListCell* arg = nullptr;
          bool first = true;

          /* Add all the arguments */
          foreach (arg, node->args) {
            auto* tle = (TargetEntry*)lfirst(arg);
            Node* n = (Node*)tle->expr;

            if (tle->resjunk)
              continue;

            if (!first)
              appendStringInfoString(buf, ", ");
            first = false;

            /* Add VARIADIC */
            if (use_variadic && lnext(node->args, arg) == nullptr)
              appendStringInfoString(buf, "VARIADIC ");

            ToStringImpl(buf, (Expr*)n);
          }
        }

        /* Add ORDER BY */
        if (node->aggorder != NIL) {
          appendStringInfoString(buf, " ORDER BY ");
          AppendAggOrderBy(node->aggorder, node->args, buf);
        }
      }

      /* Add FILTER (WHERE ..) */
      if (node->aggfilter != nullptr) {
        appendStringInfoString(buf, ") FILTER (WHERE ");
        ToStringImpl(buf, (Expr*)node->aggfilter);
      }

      appendStringInfoChar(buf, ')');
    } break;
    case T_Var: {
      const auto* node = (Var*)expr;
      appendStringInfo(buf, "#%d.%d", node->varno, node->varattno);
    } break;
    case T_ScalarArrayOpExpr: {
      const auto* node = (ScalarArrayOpExpr*)expr;
      HeapTuple tuple;
      Form_pg_operator form;
      Expr* arg1;
      Expr* arg2;

      /* Retrieve information about the operator from system catalog. */
      tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
      if (!HeapTupleIsValid(tuple))
        elog(ERROR, "cache lookup failed for operator %u", node->opno);
      form = (Form_pg_operator)GETSTRUCT(tuple);

      /* Sanity check. */
      Assert(list_length(node->args) == 2);

      /* Always parenthesize the expression. */
      appendStringInfoChar(buf, '(');

      /* Deparse left operand. */
      arg1 = (Expr*)linitial(node->args);
      ToStringImpl(buf, arg1);
      appendStringInfoChar(buf, ' ');

      /* Deparse operator name plus decoration. */
      DeparseOperatorName(buf, form);
      appendStringInfo(buf, " %s (", node->useOr ? "ANY" : "ALL");

      /* Deparse right operand. */
      arg2 = (Expr*)lsecond(node->args);
      ToStringImpl(buf, arg2);

      appendStringInfoChar(buf, ')');

      /* Always parenthesize the expression. */
      appendStringInfoChar(buf, ')');

      ReleaseSysCache(tuple);
    } break;
    case T_FuncExpr: {
      const auto* node = (FuncExpr*)expr;
      bool use_variadic;
      bool first;
      ListCell* arg;

      /*
       * If the function call came from an implicit coercion, then just show the
       * first argument.
       */
      if (node->funcformat == COERCE_IMPLICIT_CAST) {
        ToStringImpl(buf, (Expr*)linitial(node->args));
        return;
      }

      /*
       * If the function call came from a cast, then show the first argument
       * plus an explicit cast operation.
       */
      if (node->funcformat == COERCE_EXPLICIT_CAST) {
        Oid rettype = node->funcresulttype;
        int32 coerced_typmod;

        /* Get the typmod if this is a length-coercion function */
        (void)exprIsLengthCoercion((Node*)node, &coerced_typmod);

        ToStringImpl(buf, (Expr*)linitial(node->args));
        appendStringInfo(buf, "::%s", DeparseTypeName(rettype, coerced_typmod));
        return;
      }

      /* Check if need to print VARIADIC (cf. ruleutils.c) */
      use_variadic = node->funcvariadic;

      /*
       * Normal function: display as proname(args).
       */
      AppendFunctionName(node->funcid, buf);
      appendStringInfoChar(buf, '(');

      /* ... and all the arguments */
      first = true;
      foreach (arg, node->args) {
        if (!first)
          appendStringInfoString(buf, ", ");
        if (use_variadic && lnext(node->args, arg) == nullptr)
          appendStringInfoString(buf, "VARIADIC ");
        ToStringImpl(buf, (Expr*)lfirst(arg));
        first = false;
      }
      appendStringInfoChar(buf, ')');
    } break;
    case T_CoerceViaIO: {
      const auto* node = (CoerceViaIO*)expr;
      ToStringImpl(buf, node->arg);
      if (node->coerceformat != COERCE_IMPLICIT_CAST)
        appendStringInfo(buf, "::%s", DeparseTypeName(node->resulttype, -1));
    } break;
    case T_Invalid:
    case T_CaseExpr: {
      const auto* node = (CaseExpr*)expr;

      appendStringInfoString(buf, "(CASE");

      /* If this is a CASE arg WHEN then emit the arg expression */
      if (node->arg != nullptr) {
        appendStringInfoChar(buf, ' ');
        ToStringImpl(buf, node->arg);
      }

      /* Add each condition/result of the CASE clause */
      foreach_node(CaseWhen, whenclause, node->args) {
        /* WHEN */
        appendStringInfoString(buf, " WHEN ");
        if (node->arg == nullptr) /* CASE WHEN */
          ToStringImpl(buf, whenclause->expr);
        else /* CASE arg WHEN */
          /* Ignore the CaseTestExpr and equality operator. */
          ToStringImpl(buf, lsecond(castNode(OpExpr, whenclause->expr)->args));

        /* THEN */
        appendStringInfoString(buf, " THEN ");
        ToStringImpl(buf, whenclause->result);
      }

      /* add ELSE if present */
      if (node->defresult != nullptr) {
        appendStringInfoString(buf, " ELSE ");
        ToStringImpl(buf, node->defresult);
      }

      /* append END */
      appendStringInfoString(buf, " END)");
    } break;
    case T_RelabelType: {
      const auto* node = (RelabelType*)expr;
      ToStringImpl(buf, node->arg);
      if (node->relabelformat != COERCE_IMPLICIT_CAST)
        appendStringInfo(buf, "::%s", DeparseTypeName(node->resulttype, node->resulttypmod));
    } break;
    default:
      appendStringInfoString(buf, "Invalid");
  }
}

std::string NodeUtils::ToString(const void* expr) {
  StringInfo buf = makeStringInfo();
  pg_guard([&buf, expr] { ToStringImpl(buf, expr); });
  auto str = std::string(buf->data, buf->len);
  destroyStringInfo(buf);
  return str;
}

}  // namespace optimizer